<?php

/**
 * Class to interact with the gnuPG.
 *
 * @package   gnuPG_class
 * @author    Enrique Garcia Molina <egarcia@egm.as>
 * @copyright Copyright (c) 2004-2005, EGM :: Ingenieria sin fronteras
 * @license   GNU LGPL (http://www.gnu.org/copyleft/lesser.html)
 * @since     Viernes, Enero 30, 2004
 * @version   $Id: gnuPG_class.inc,v 1.0.9 2005-07-20 11:59:00-05 egarcia Exp $
 * @see       readme.txt
 */
class gnuPG
{
	/**
	* the path to gpg executable (default: /usr/local/bin/gpg)
	* @access private
	* @var string
	*/
	var $program_path;
	
	/**
	* The path to directory where personal gnupg files (keyrings, etc) are stored (default: ~/.gnupg)
	* @access private
	* @var string
	*/
	var $home_directory;
	
	/**
	* Error and status messages
	* @var string
	*/
	var $error;
	
	/**
	* Output message
	* @var string
	*/
	var $output;
	
	/**
	* Create the gnuPG object.
	*
	* Set the program path for the GNUPG and the home directory of the keyring.
	* If this parameters are not specified, according to the OS the function derive the values.
	*
	* @param  string $program_path   Full program path for the GNUPG
	* @param  string $home_directory Home directory of the keyring
	* @return void
	*/
	function gnuPG($program_path = false, $home_directory = false)
	{
		// if is empty then assume the path based in the OS
		if (empty($program_path)) {
			if ( strstr(PHP_OS, 'WIN') )
				$program_path = 'C:\gnupg\gpg';
			else
				$program_path = '/usr/local/bin/gpg';
		}
		$this->program_path = $program_path;
		
		// if is empty the home directory then assume based in the OS
		if (empty($home_directory)) {
			if ( strstr(PHP_OS, 'WIN') )
				$home_directory = 'C:\gnupg';
			else
				$home_directory = '~/.gnupg';
		}
		$this->home_directory = $home_directory;
	}
	
	/**
	* Call a subprogram redirecting the standard pipes
	*
	* @access private
	* @param  string $command The full command to execute
	* @param  string $input   The input data
	* @param  string $output  The output data
	* @return bool   true on success, false on error
	*/
	function _fork_process($command, $input = false, &$output)
	{
		// define the redirection pipes
		$descriptorspec = array(
			0 => array("pipe", "r"),  // stdin is a pipe that the child will read from
			1 => array("pipe", "w"),  // stdout is a pipe that the child will write to
			2 => array("pipe", "w")   // stderr is a pipe that the child will write to
		);
		$pipes = null;
		
		// calls the process
		$process = proc_open($command, $descriptorspec, $pipes);
		if (is_resource($process)) {
			// writes the input
			if (!empty($input)) fwrite($pipes[0], $input);
			fclose($pipes[0]);
			
			// reads the output
			while (!feof($pipes[1])) {
				$data = fread($pipes[1], 1024);
				if (strlen($data) == 0) break;
				$output .= $data;
			}
			fclose($pipes[1]);
			
			// reads the error message
			$result = '';
			while (!feof($pipes[2])) {
				$data = fread($pipes[2], 1024);
				if (strlen($data) == 0) break;
				$result .= $data;
			}
			fclose($pipes[2]);
			
			// close the process
			$status = proc_close($process);
			
			// returns the contents
			$this->error = $result;
			return ($status == 0);
		} else {
			$this->error = 'Unable to fork the command';
			return false;
		}
	}
	
	/**
	* Get the keys from the KeyRing.
	*
	* The returned array get the following elements:
	* [RecordType, CalculatedTrust, KeyLength, Algorithm,
	*  KeyID, CreationDate, ExpirationDate, LocalID,
	*  Ownertrust, UserID]
	*
	* @param  string $KeyKind the kind of the keys, can be secret or public
	* @return mixed  false on error, the array with the keys in the keyring in success
	*/
	function ListKeys($KeyKind = 'public')
	{
		// validate the KeyKind
		$KeyKind = strtolower(substr($KeyKind, 0, 3));
		if (($KeyKind != 'pub') && ($KeyKind != 'sec')) {
			$this->error = 'The Key kind must be public or secret';
			return false;
		}
		
		// initialize the output
		$contents = '';
		
		// execute the GPG command
		if ( $this->_fork_process($this->program_path . ' --homedir ' . $this->home_directory .
				' --with-colons ' . (($KeyKind == 'pub') ? '--list-public-keys': '--list-secret-keys'),
			false, $contents) ) {
			
			// initialize the array data
			$returned_keys = array();
			
			// the keys are \n separated
			$contents = explode("\n", $contents);
			
			// find each key
			foreach ($contents as $data) {
				// read the fields to get the : separated, the sub record is dismiss
				$fields = explode(':', $data);
				if (count($fields) <= 3) continue;
				
				// verify the that the record is valid
				if (($fields[0] == 'pub') || ($fields[0] == 'sec')) {
					array_push($returned_keys, array(
						'RecordType' => $fields[0],
						'CalculatedTrust' => $fields[1],
						'KeyLength' => $fields[2],
						'Algorithm' => $fields[3],
						'KeyID' => $fields[4],
						'CreationDate' => $fields[5],
						'ExpirationDate' => $fields[6],
						'LocalID' => $fields[7],
						'Ownertrust' => $fields[8],
						'UserID' => $fields[9]
						)
					);
				}
			}
			return $returned_keys;
		} else
			return false;
	}
	
	/**
	* Export a key.
	*
	* Export all keys from all keyrings, or if at least one name is given, those of the given name.
	*
	* @param  string $KeyID  The Key ID to export
	* @return mixed  false on error, the key block with the exported keys
	*/
	function Export($KeyID = false)
	{
		$KeyID = empty($KeyID) ? '': $KeyID;
		
		// initialize the output
		$contents = '';
		
		// execute the GPG command
		if ( $this->_fork_process($this->program_path . ' --homedir ' . $this->home_directory .
				' --armor --export ' . $KeyID,
			false, $contents) )
			return (empty($contents) ? false: $contents);
		else
			return false;
	}
	
	/**
	* Import/merge keys.
	*
	* This adds the given keys to the keyring. New keys are appended to your
	* keyring and already existing keys are updated. Note that GnuPG does not
	* import keys that are not self-signed.
	*
	* @param  string $KeyBlock  The PGP block with the key(s).
	* @return mixed  false on error, the array with [KeyID, UserID] elements of imported keys on success.
	*/
	function Import($KeyBlock)
	{
		// Verify for the Key block contents
		if (empty($KeyBlock)) {
			$this->error = 'No valid key block was specified.';
			return false;
		}
		
		// initialize the output
		$contents = '';
		
		// execute the GPG command
		if ( $this->_fork_process($this->program_path . ' --homedir ' . $this->home_directory .
				' --status-fd 1 --import',
			$KeyBlock, $contents) ) {
			// initialize the array data
			$imported_keys = array();
			
			// parse the imported keys
			$contents = explode("\n", $contents);
			foreach ($contents as $data) {
				$matches = false;
				if (preg_match('/\[GNUPG:\]\sIMPORTED\s(\w+)\s(.+)/', $data, $matches))
					array_push($imported_keys, array(
						'KeyID' => $matches[1],
						'UserID' => $matches[2]));
			}
			return $imported_keys;
		} else
			return false;
	}
	
	/**
	* Generate a new key pair.
	*
	* @param  string $RealName     The real name of the user or key.
	* @param  string $Comment      Any explanatory commentary.
	* @param  string $Email        The e-mail for the user.
	* @param  string $Passphrase   Passphrase for the secret key, default is not to use any passphrase.
	* @param  string $ExpireDate   Set the expiration date for the key (and the subkey).  It may either be entered in ISO date format (2000-08-15) or as number of days, weeks, month or years (<number>[d|w|m|y]). Without a letter days are assumed.
	* @param  string $KeyType      Set the type of the key, the allowed values are DSA and RSA, default is DSA.
	* @param  int    $KeyLength    Length of the key in bits, default is 1024.
	* @param  string $SubkeyType   This generates a secondary key, currently only one subkey can be handled ELG-E.
	* @param  int    $SubkeyLength Length of the subkey in bits, default is 1024.
	* @return mixed  false on error, the fingerprint of the created key pair in success
	*/
	function GenKey($RealName, $Comment, $Email, $Passphrase = '', $ExpireDate = 0, $KeyType = 'DSA', $KeyLength = 1024, $SubkeyType = 'ELG-E', $SubkeyLength = 1024)
	{
		// validates the keytype
		if (($KeyType != 'DSA') && ($KeyType != 'RSA')) {
			$this->error = 'Invalid Key-Type, the allowed are DSA and RSA';
			return false;
		}
		
		// validates the subkey
		if ((!empty($SubkeyType)) && ($SubkeyType != 'ELG-E')) {
			$this->error = 'Invalid Subkey-Type, the allowed is ELG-E';
			return false;
		}
		
		// validate the expiration date
		if (!preg_match('/^(([0-9]+[dwmy]?)|([0-9]{4}-[0-9]{2}-[0-9]{2}))$/', $ExpireDate)) {
			$this->error = 'Invalid Expire Date, the allowed values are <iso-date>|(<number>[d|w|m|y])';
			return false;
		}
		
		// generates the batch configuration script
		$batch_script  = "Key-Type: $KeyType\n" .
			"Key-Length: $KeyLength\n";
		if (($KeyType == 'DSA') && ($SubkeyType == 'ELG-E'))
			$batch_script .= "Subkey-Type: $SubkeyType\n" .
				"Subkey-Length: $SubkeyLength\n";
		$batch_script .= "Name-Real: $RealName\n" .
			"Name-Comment: $Comment\n" .
			"Name-Email: $Email\n" .
			"Expire-Date: $ExpireDate\n" .
			"Passphrase: $Passphrase\n" .
			"%commit\n" .
			"%echo done with success\n";
		
		// initialize the output
		$contents = '';
		
		// execute the GPG command
		if ( $this->_fork_process($this->program_path . ' --homedir ' . $this->home_directory .
				' --batch --status-fd 1 --gen-key',
			$batch_script, $contents) ) {
			$matches = false;
			if ( preg_match('/\[GNUPG:\]\sKEY_CREATED\s(\w+)\s(\w+)/', $contents, $matches) )
				return $matches[2];
			else
				return true;
		} else
			return false;
	}
	
	/**
	* Encrypt and sign data.
	*
	* @param  string $KeyID          the key id used to encrypt
	* @param  string $Passphrase     the passphrase to open the key used to encrypt
	* @param  string $RecipientKeyID the recipient key id
	* @param  string $Text           data to encrypt
	* @return mixed  false on error, the encrypted data on success
	*/
	function Encrypt($KeyID, $Passphrase, $RecipientKeyID, $Text)
	{
		// initialize the output
		$contents = '';
		
		// execute the GPG command
		if ( $this->_fork_process($this->program_path . ' --homedir ' . $this->home_directory .
				' --armor --passphrase-fd 0 --yes --batch --force-v3-sigs --trust-model classic' .
				" --local-user $KeyID --default-key $KeyID --recipient $RecipientKeyID --sign --encrypt",
			$Passphrase . "\n" . $Text, $contents) )
			return $contents;
		else
			return false;
	}
	
	/**
	* Decrypt the data.
	*
	* If the decrypted file is signed, the signature is also verified.
	*
	* @param  string $KeyID      the key id to decrypt
	* @param  string $Passphrase the passphrase to open the key used to decrypt
	* @param  string $Text       data to decrypt
	* @return mixed  false on error, the clear (decrypted) data on success
	*/
	function Decrypt($KeyID, $Passphrase, $Text)
	{
		// the text to decrypt from another platforms can has a bad sequence
		// this line removes the bad date and converts to line returns
		$Text = preg_replace("/\x0D\x0D\x0A/s", "\n", $Text);
		
		// we generate an array and add a new line after the PGP header
		$Text = explode("\n", $Text);
		if (count($Text) > 1) $Text[1] .= "\n";
		$Text = implode("\n", $Text);
		
		// initialize the output
		$contents = '';
		
		// execute the GPG command
		if ( $this->_fork_process($this->program_path . ' --homedir ' . $this->home_directory .
				' --passphrase-fd 0 --yes --batch --trust-model classic' .
				" --local-user $KeyID --default-key $KeyID --decrypt",
			$Passphrase . "\n" . $Text, $contents) )
			return $contents;
		else
			return false;
	}
	
	/**
	* Remove key from the public keyring.
	*
	* If secret is specified it try to remove the key from from the secret
	* and public keyring.
	* The returned error codes are:
	* 1 = no such key
	* 2 = must delete secret key first
	* 3 = ambiguos specification
	*
	* @param  string $KeyID   the key id to be removed, if this is the secret key you must specify the fingerprint
	* @param  string $KeyKind the kind of the keys, can be secret or public
	* @return mixed  true on success, otherwise false or the delete error code
	*/
	function DeleteKey($KeyID, $KeyKind = 'public')
	{
		if (empty($KeyID)) {
			$this->error = 'You must specify the KeyID to delete';
			return false;
		}
		
		// validate the KeyKind
		$KeyKind = strtolower(substr($KeyKind, 0, 3));
		if (($KeyKind != 'pub') && ($KeyKind != 'sec')) {
			$this->error = 'The Key kind must be public or secret';
			return false;
		}
		
		// initialize the output
		$contents = '';
		
		// execute the GPG command
		if ( $this->_fork_process($this->program_path . ' --homedir ' . $this->home_directory .
				' --batch --yes --status-fd 1 ' .
				(($KeyKind == 'pub') ? '--delete-key ': '--delete-secret-keys ') . $KeyID,
			false, $contents) )
			return true;
		else {
			$matches = false;
			if ( preg_match('/\[GNUPG:\]\DELETE_PROBLEM\s(\w+)/', $contents, $matches) )
				return $matches[1];
			else
				return false;
		}
	}
	
	/**
	* Make a signature on key.
	*
	* If the key is not yet signed by the specified user.
	*
	* @param  string $KeyID       the key id used to sign
	* @param  string $Passphrase  the passphrase to open the key used to sign
	* @param  string $KeyIDToSign the key to be signed
	* @param  int    $CheckLevel  the check level (0, 1, 2, 3 -casual to extensive-)
	* @return bool   true on success, otherwise false
	*/
	function SignKey($KeyID, $Passphrase, $KeyIDToSign, $CheckLevel = 0)
	{
		$contents = '';
		
		// validates the check level
		$CheckLevel = intval($CheckLevel);
		if (($CheckLevel < 0) || ($CheckLevel > 3)) {
			$this->error = 'Invalid Check-Level, the allowed are 0, 1, 2, 3';
			return false;
		}
		
		// execute the GPG command
		if ( $this->_fork_process($this->program_path . ' --homedir ' . $this->home_directory .
				' --passphrase-fd 0 --status-fd 1 --yes --batch' .
				" --default-cert-check-level $CheckLevel --default-key $KeyID --edit-key $KeyIDToSign sign save",
			$Passphrase . "\n", $contents) ) {
			$matches = false;
			if ( preg_match('/\[GNUPG:\]\s[ALREADY_SIGNED|GOOD_PASSPHRASE]/', $contents, $matches) )
				return true;
			else
				return false;
		} else
			return false;
	}
}

?>